﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using TextPlus.Extensions;

namespace TextPlus
{
    public class TextBlockPlusLine : StackPanel
    {
        private readonly TextBlockPlus _parent;
        private readonly double _maxWidth;
        private TextBlock _currentTextBlock;

        internal TextBlockPlusLine(TextBlockPlus parent, double indent, double maxWidth)
        {
            _parent = parent;
            Margin = new Thickness(indent, 0, 0, 0);
            Orientation = Orientation.Horizontal;
            _maxWidth = maxWidth;
        }

        internal bool Add(Inline inline, out Inline remainder)
        {
            bool result = false;
            remainder = null;
            bool isFirstElement = (Children.Count == 0);
            if (inline is Link)
            {
                Link remainderLink;
                result = Add((Link) inline, isFirstElement, out remainderLink);
                remainder = remainderLink;
            }
            else if (inline is Run)
            {
                Run remainderRun;
                result = Add((Run) inline, isFirstElement, out remainderRun);
                remainder = remainderRun;
            }
            return result;
        }

        private bool Add(Link link, bool isFirstElement, out Link remainder)
        {
            var oldTextBlock = _currentTextBlock;
            _currentTextBlock = null;
            remainder = null;

            Children.Add(link.LinkElement);
            if (!IsTooBig() || TextWrapping.NoWrap == _parent.TextWrapping)
                return true;

            if (TrimTextToFit(link, isFirstElement, out remainder))
                return true;

            _currentTextBlock = oldTextBlock;
            Children.Remove(link.LinkElement);
            return false;
        }

        private bool Add(Run run, bool isFirstElement, out Run remainder)
        {
            bool createdCurrentTextBlock = false;
            if (_currentTextBlock == null)
            {
                createdCurrentTextBlock = true;
                CreateCurrentTextBlock();
            }
            remainder = null;
            
            _currentTextBlock.Inlines.Add(run);
            if (!IsTooBig() || TextWrapping.NoWrap == _parent.TextWrapping)
                return true;

            if (TrimTextToFit(run, isFirstElement, out remainder))
                return true;

            if (createdCurrentTextBlock)
                Children.Remove(_currentTextBlock);
            return false;
        }

        private void CreateCurrentTextBlock()
        {
            _currentTextBlock = new TextBlock();
            Children.Add(_currentTextBlock);
        }

        private bool TrimTextToFit<T>(T inline, bool isFirstElement, out T remainder) where T : Inline
        {
            remainder = null;

            //make sure we break at the right place
            var originalText = GetText(inline);
            int shortLength = 0;
            int longLength = originalText.Length;
            int testLength;
            while (longLength - shortLength > 1)
            {
                testLength = shortLength + ((longLength - shortLength) / 2);
                SetText(inline, originalText.Substring(0, testLength));

                if (IsTooBig())
                    longLength = testLength;
                else
                    shortLength = testLength;
            }
            SetText(inline, originalText.Substring(0, shortLength));

            //only break on whitespace
            while (GetText(inline).Length > 0 && !LastCharIsWhiteSpace(inline))
                TrimLastChar(inline);

            //force at least one element per line
            if (GetText(inline).Length == 0 && isFirstElement)
            {
                int length = 0;
                do
                {
                    length++;
                    SetText(inline, originalText.Substring(0, length));
                } while (!IsTooBig());

                if (length > 1 && IsTooBig())
                    SetText(inline, originalText.Substring(0, length - 1));
            }

            if (GetText(inline).Length == 0 || (!isFirstElement && IsTooBig()))
                return false;

            //force whitespace at break to end of this line instead of beginning of next line
            while (GetText(inline).Length > originalText.Length && char.IsWhiteSpace(originalText, GetText(inline).Length))
                SetText(inline, originalText.Substring(0, GetText(inline).Length + 1));

            //get remainder
            if (GetText(inline).Length < originalText.Length)
            {
                remainder = (T)inline.Clone(_parent);
                var overflowText = originalText.Substring(GetText(inline).Length);
                //if we broke the first line on a line break, don't repeat it
                if (overflowText.Substring(0, 1) == "\n")
                    overflowText = overflowText.Substring(1);
                SetText(remainder, overflowText);
            }

            return true;
        }

        private static void TrimLastChar(Inline inline)
        {
            var text = GetText(inline);
            SetText(inline, text.Substring(0, text.Length - 1));
        }

        private static bool LastCharIsWhiteSpace(Inline inline)
        {
            var text = GetText(inline);
            return text.Length != 0 && char.IsWhiteSpace(text, text.Length - 1);
        }

        private static string GetText(Inline inline)
        {
            if (inline is Run)
                return ((Run)inline).Text;
            if (inline is Link)
                return ((Link)inline).Text;
            return null;
        }

        private static void SetText(Inline inline, string text)
        {
            if (inline is Run)
                ((Run)inline).Text = text;
            else if (inline is Link)
                ((Link)inline).Text = text;
        }

        private bool IsTooBig()
        {
            Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
            return (DesiredSize.Width > _maxWidth);
        }
    }
}